package org.budo.graph.druid.dubbo.filter;

import java.util.Map;

import org.budo.dubbo.rpc.util.InvocationUtil;
import org.budo.graph.Constant;
import org.budo.graph.service.util.SessionRequestUtil;
import org.budo.support.lang.util.MapUtil;

import com.alibaba.druid.filter.stat.StatFilterContext;
import com.alibaba.druid.support.http.stat.WebAppStat;
import com.alibaba.druid.support.http.stat.WebAppStatManager;
import com.alibaba.druid.support.http.stat.WebRequestStat;
import com.alibaba.druid.support.http.stat.WebSessionStat;
import com.alibaba.druid.support.http.stat.WebURIStat;
import com.alibaba.druid.support.profile.ProfileEntryKey;
import com.alibaba.druid.support.profile.ProfileEntryReqStat;
import com.alibaba.druid.support.profile.Profiler;
import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.RpcException;

import lombok.extern.slf4j.Slf4j;

/**
 * @author lmw
 * @see com.alibaba.druid.support.http.WebStatFilter
 * @see com.alibaba.druid.support.spring.mvc.StatHandlerInterceptor
 */
@Slf4j
@Activate(group = { Constants.PROVIDER })
public class BudoGraphDruidDubboProviderSideFilter extends AbstractBudoGraphDruidDubboFilter implements Filter {
    private static final String CONTEXT_PATH = "";

    private static final String REQUEST_PRINCIPAL = "";

    private static final Integer MAX_STAT_SESSION_COUNT = 999;

    protected BudoGraphDruidWebStatFilterContextListener contextListener = new BudoGraphDruidWebStatFilterContextListener();

    protected WebAppStat webAppStat = null;

    public WebAppStat getWebAppStat() {
        if (null != this.webAppStat) {
            return this.webAppStat;
        }

        synchronized (BudoGraphDruidDubboProviderSideFilter.class) {
            if (null != this.webAppStat) {
                return this.webAppStat;
            }

            this.webAppStat = new WebAppStat(CONTEXT_PATH, MAX_STAT_SESSION_COUNT);
            WebAppStatManager.getInstance().addWebAppStatSet(this.webAppStat); // addWebAppStatSet
            StatFilterContext.getInstance().addContextListener(this.contextListener); // addContextListener

            return this.webAppStat;
        }
    }

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String requestSessionId = this.getRequestSessionId(invoker, invocation);
        String requestURI = InvocationUtil.invocationToRequestUri(invocation);

        // 记录单个客户端请求了哪些接口
        SessionRequestUtil.recordSessionRequest(requestSessionId, requestURI);

        long startNano = System.nanoTime();
        long startMillis = System.currentTimeMillis();

        WebRequestStat requestStat = new WebRequestStat(startNano, startMillis);
        WebRequestStat.set(requestStat);

        WebSessionStat sessionStat = this.getSessionStat(invoker, invocation, requestSessionId);
        this.getWebAppStat().beforeInvoke();

        WebURIStat uriStat = this.getWebAppStat().getURIStat(requestURI, false);

        Profiler.initLocal();
        Profiler.enter(requestURI, Profiler.PROFILE_TYPE_WEB);

        // 第一次访问时，uriStat这里为null，是为了防止404攻击。
        if (uriStat != null) {
            uriStat.beforeInvoke();
        }

        // 第一次访问时，sessionId为null，如果缺省sessionCreate=false，sessionStat就为null。
        if (sessionStat != null) {
            sessionStat.beforeInvoke();
        }

        Throwable error = null;
        try {
            Result result = invoker.invoke(invocation);
            return result;
        } catch (Throwable e) {
            error = e;
            throw e;
        } finally {
            long endNano = System.nanoTime();
            requestStat.setEndNano(endNano);

            long nanos = endNano - startNano;
            this.getWebAppStat().afterInvoke(error, nanos);

            if (sessionStat == null) {
                sessionStat = this.getSessionStat(invoker, invocation, requestSessionId);
                if (sessionStat != null) {
                    sessionStat.beforeInvoke(); // 补偿
                }
            }

            if (sessionStat != null) {
                sessionStat.afterInvoke(error, nanos);
                sessionStat.setPrincipal(REQUEST_PRINCIPAL);
            }

            if (uriStat == null) {
                uriStat = this.getWebAppStat().getURIStat(requestURI, true);

                if (uriStat != null) {
                    uriStat.beforeInvoke(); // 补偿调用
                }
            }

            if (uriStat != null) {
                uriStat.afterInvoke(error, nanos);
            }

            WebRequestStat.set(null);
            this.profilerRelease(nanos, invoker, invocation);

            Map<ProfileEntryKey, ProfileEntryReqStat> requestStatsMap = Profiler.getStatsMap();
            if (uriStat != null) {
                uriStat.getProfiletat().record(requestStatsMap);
            }

            Profiler.removeLocal();
        }
    }

    private void profilerRelease(long nanos, Invoker<?> invoker, Invocation invocation) {
        try {
            // System.err.println(">>>" + Profiler.current());
            Profiler.release(nanos);
        } catch (Throwable e) {
            log.error("#152 profilerRelease error, invoker=" + invoker + ", invocation=" + invocation + ", e=" + e, e);
            throw new RuntimeException(e);
        }
    }

    private WebSessionStat getSessionStat(Invoker<?> invoker, Invocation invocation, String requestSessionId) {
        WebSessionStat webSessionStat = null;

        if (null != requestSessionId) {
            webSessionStat = this.getWebAppStat().getSessionStat(requestSessionId, true);
        }

        if (null == webSessionStat) {
            return null;
        }

        long currentMillis = System.currentTimeMillis();
        String userAgent = this.getUserAgent(invoker, invocation);
        webSessionStat.setUserAgent(userAgent);

        String remoteAddress = this.getRemoteAddress(invoker, invocation);
        webSessionStat.addRemoteAddress(remoteAddress);

        if (webSessionStat.getCreateTimeMillis() == -1L) {
            webSessionStat.setCreateTimeMillis(currentMillis);

            this.getWebAppStat().computeUserAgent(userAgent);
            this.getWebAppStat().incrementSessionCount();
        }

        return webSessionStat;
    }

    /**
     * 从 Attachment 或 RpcContext 读取
     */
    private String getRemoteAddress(Invoker<?> invoker, Invocation invocation) {
        String remoteAddress = invocation.getAttachment(Constant.DruidDubbo.REMOTE_ADDRESS_KEY);
        if (null != remoteAddress) {
            return remoteAddress;
        }

        RpcContext context = RpcContext.getContext();
        if (null == context) {
            return "RPC_CONTEXT_IS_NULL_1";
        }

        String remoteHost = context.getRemoteHost();
        if (null == remoteHost) {
            return "REMOTE_HOST_IS_NULL_2";
        }

        return remoteHost;
    }

    /**
     * 从 Attachment 或 RpcContext 读取
     */
    private String getRequestSessionId(Invoker<?> invoker, Invocation invocation) {
        String requestSessionId = invocation.getAttachment(Constant.DruidDubbo.SESSION_ID_KEY);
        if (null != requestSessionId) {
            return requestSessionId;
        }

        // from RpcContext
        RpcContext context = RpcContext.getContext();
        if (null == context) {
            return "RPC_CONTEXT_IS_NULL_3";
        }

        String remoteHost = context.getRemoteHost();
        if (null == remoteHost) {
            return "REMOTE_HOST_IS_NULL_4";
        }

        return remoteHost;
    }

    private String getUserAgent(Invoker<?> invoker, Invocation invocation) {
        String userAgent = invocation.getAttachment(Constant.DruidDubbo.USER_AGENT_KEY);
        if (null != userAgent) {
            return userAgent;
        }

        String attachments = MapUtil.toString(invocation.getAttachments());
        return "invocation.attachments=" + attachments;
    }
}